﻿/// <reference path="Xpcom.js" />
///--------------------------------------------------------------------
/// <file name="FXDom.js">
///   Provides DOM utilities in the context of a specific document.
///   This borrows heavily from ideas in jQuery, but is scoped to only
///   Mozilla Firefox and only the features that are needed for NAMFox.
/// </file>
///--------------------------------------------------------------------

NAMFox.ns(function() { with (NAMFox) {

var attrExpr = /(\S*)\[(\S*?)([\$\^\*\!]?=)([\s\S]*?[^\\])\](\S*)/;
var classExpr = /(\S*)\.(\S*)/;
var escapeExpr = /\\(.)/g;
var filterExpr = /(\S*):(?![^\[]*?\])([\s\S]*)/;
var filterDataExpr = /([^\(\s]*)\(([^\)]*)\)/;
var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^(\S*?)(#|\.)([\w-]+)$/;
var splitExpr = / (?![^\(\[]*?[\)\]])/g;

this.FXDom = this.$FX = function(selector, context) {
    /// <param name="selector">
    ///   1. #ID - The ID of a specific element.
    ///   2. .class - Specifies to retrieve elements with the specified class name.
    ///   3. [attr=value] - Specifies to retrieve elements where the attribute 'attr' equals 'value'.
    ///   4. DOM elements
    /// </param>
    /// <param name="context" optional="true" domElement="true">
    ///    (Optional) The HTMLDocument instance or an element in a document in which to look
    ///    for the elements specified by the selector.
    ///    Required if the selector is not a DOM element.
    /// </param>
    /// <field name="length" type="Number" integer="true">The number of DOM elements in this FXDom instance.</field>
    /// <returns type="FXDom" />

    return new FXDom.prototype.init(selector, context);
}

FXDom.prototype = {
    _document: null,
    length: 0,
    
    any: function() {
        /// <summary>
        /// Returns true if any elements are currently selected.
        /// </summary>
        /// <returns type="Boolean"/>
        
        return this.length > 0;
    },
    
    each: function(callback) {
        /// <summary>Iterates the collection of DOM elements encapsulated by this FXDom object and applies a callback to each one.</summary>
        /// <param name="callback" type="Function">The function to call each element with.</param>
        /// <returns type="FXDom" />
        
        return FXDom.each(this, callback);
    },
    
    init: function(selector, context) {
        /// <summary>Selects elements with the given expression.</summary>
        /// <param name="selector" type="String">
        ///   1. #ID - The ID of a specific element.
        ///   2. .class - Specifies to retrieve elements with the specified class name.
        ///   3. DOM elements
        /// </param>
        /// <returns type="FXDom" />
        
        if (!context) {
            if (selector.ownerDocument) {
                context = selector.ownerDocument;
            }
            else if (selector[0] && selector[0].ownerDocument) {
                context = selector[0].ownerDocument;
            }
            else {
                context = document;
            }
        }
        
        this._document = context.ownerDocument || context;
        
        selector = selector || this._document;
        
        if (selector.nodeType) {
            this[0] = selector;
            this.length = 1;
            return this;
        }
        
        if (typeof(selector) === "string") {
            return FXDom(context, this._document).find(selector);
        }

        return this.setArray(FXDom.makeArray(selector));
    },
    
    setArray: function(array) {
        /// <summary>Completely overwrites the elements internalized by this FXDom instance with the contents of array.</summary>
        /// <param name="array" type="Array">An array of DOM elements.</param>
        /// <returns type="FXDom" />
        
        var length = array.length;
        
        if (length === undefined || array.split || array.setInterval || array.call) {
            this[0] = array;
        }
        else {
            this.length = 0;
            Array.prototype.push.apply(this, array);
        }
        
        return this;
    },
    
    find: function(selector) {
        var selectorList = selector.split(splitExpr);
        var elements = {};
        var contexts = this;
        
        for (var i = 0; elements && i < selectorList.length; ++i) {
            selector = selectorList[i];
            if (i > 0) {
                contexts = elements;
            }
            var match = quickExpr.exec(selector);
            
            if (match) {
                if (match[1]) {
                    elements = this.clean([match[1]], this._document);
                }
                else if (match[4] === "#") {
                    elements = [ this._document.getElementById(match[5]) ];
                }
                else if (match[4] === ".") {
                    var tagName = match[3];
                    var className = " " + selector.substr(tagName.length + 1, selector.length) + " ";
                    
                    elements = iterateElements(function(element) {
                        if (!element.className) {
                             return false;
                        }
                        return $.contains(" " + element.className + " ", className);
                    },
                    tagName || "*");
                }
            }
            else {
                match = attrExpr.exec(selector);
                
                var attrPredicate = null, filterPredicate = null, listFilter = null;
                
                if (match) {
                    var tagName = match[1];
                    var attr = match[2];
                    var comparison = match[3];
                    var value = match[4];
                    value = value.replace(escapeExpr, "$1");
                    
                    var comparator = null;
                    switch (comparison) {
                        case "^=":
                            comparator = function(attr) {
                                return attr.indexOf(value) === 0;
                            };
                            break;
                        case "$=":
                            comparator = function(attr) {
                                return attr.indexOf(value) === (attr.length - value.length);
                            };
                            break;
                        case "*=":
                            comparator = function(attr) {
                                return $.contains(attr, value);
                            };
                            break;
                        case "=":
                            comparator = function(attr) {
                                return attr === value;
                            };
                            break;
                        case "!=":
                            comparator = function(attr) {
                                return attr !== value;
                            };
                            break;
                    }
                    
                    attrPredicate = function(element) {
                        return comparator(element.getAttribute(attr) || element[attr]);
                    };
                    
                    selector = match[1] + match[5];
                }
                match = filterExpr.exec(selector);
                
                if (match) {
                    var tagName = match[1];
                    var className = "";
                    var filter = match[2];
                    var filterMatch = filterDataExpr.exec(filter);
                    var filterData = null;
                    
                    if (filterMatch) {
                        filter = filterMatch[1];
                        filterData = filterMatch[2];
                    }
                    
                    var classMatch = tagName.match(classExpr);
                    if (classMatch) {
                        tagName = classMatch[1] || "*";
                        className = classMatch[2];
                    }
                    
                    var filterElement = FXDom.filters[filter];
                    
                    filterPredicate = function (element, index) {
                        var result = true;
                        if (filterElement.filterInline) {
                            result = filterElement.filterInline(element, filterData, index);
                        }
                        
                        if (className) {
                            result = $.contains(" " + element.className + " ", className);
                        }
                        
                        return result;
                    };
                    
                    listFilter = filterElement.filterList &&
                        function (matches) {
                            return filterElement.filterList(matches, filterData);
                        }
                        ||
                        null;
                    
                }
                
                if (attrPredicate || filterPredicate) {
                    elements = iterateElements(
                        function(element, index) {
                            var result = true;
                            
                            if (attrPredicate) {
                                result = attrPredicate(element, index);
                                
                                if (!result) {
                                    return false;
                                }
                            }
                            
                            if (filterPredicate) {
                                result = filterPredicate(element, index);
                            }
                            
                            return result;
                        },
                        tagName,
                        listFilter
                    );
                }
                else {
                    elements = iterateElements(null, selector);
                }
            }
            
            if (!elements || elements.length === 0 || !elements[0]) {
                break;
            }
        }
        
        function iterateElements(predicate, tagName, filterAction) {
            var matches = [];
            
            for (var j = 0; j < contexts.length; ++j) {
                var elements = contexts[j].getElementsByTagName(tagName || "*");
                var contextMatches = [];
                
                for (var i = 0; i < elements.length; ++i) {
                    if (!predicate || predicate(elements[i], i)) {
                        contextMatches.push(elements[i]);
                    }
                }
                
                if (filterAction) {
                    contextMatches = filterAction(contextMatches);
                }
                
                matches = matches.concat(contextMatches);
            }
            
            return matches;
        }
        
        if (!elements[0]) {
            elements = [];
        }
        
        return $FX(elements, this._document);
    },
    
    clean: function(elements, context) {
        var ret = [];
        context = context || this._document;
        // !context.createElement fails in IE with an error but returns typeof 'object'
        if (typeof context.createElement == 'undefined')
            context = context.ownerDocument || context[0] && context[0].ownerDocument || this._document;

        FXDom.each(elements, function(i, element){
            if (!element)
                return;

            if (element.constructor === Number)
                element += '';

            // Convert html string into DOM nodes
            if (typeof(element) === "string") {
                // Fix "XHTML"-style tags in all browsers
                element = element.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
                    return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
                        all :
                        front + "></" + tag + ">";
                });

                // Trim whitespace, otherwise indexOf won't work as expected
                var tags = $.trim(element).toLowerCase(), div = context.createElement("div");
                var wrap =
                    // option or optgroup
                    !tags.indexOf("<opt") &&
                    [ 1, "<select multiple='multiple'>", "</select>" ] ||

                    !tags.indexOf("<leg") &&
                    [ 1, "<fieldset>", "</fieldset>" ] ||

                    tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
                    [ 1, "<table>", "</table>" ] ||

                    !tags.indexOf("<tr") &&
                    [ 2, "<table><tbody>", "</tbody></table>" ] ||

                     // <thead> matched above
                    (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
                    [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||

                    !tags.indexOf("<col") &&
                    [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||

                    [ 0, "", "" ];

                // Go to html and back, then peel off extra wrappers
                // Warning: innerHTML should not be set dynamically:
                //   This code is taken from the jQuery implementation
                //   http://code.jquery.com/jquery-1.6.1.js line 6064
                div.innerHTML = wrap[1] + element + wrap[2];
                
                // Move to the right depth
                while (wrap[0]--) {
                    div = div.lastChild;
                }
                
                element = FXDom.makeArray(div.childNodes);
            }

            if (element.length === 0 && !FXDom.nodeName(element, "form") && !FXDom.nodeName(element, "select")) {
                return;
            }

            if (element[0] === undefined || FXDom.nodeName(element, "form") || element.options) {
                ret.push(element);
            }
            else {
                ret = ret.concat(element);
            }

        });

        return ret;
    },
    
    clone: function(element) {
        return $FX.clone(element);
    },
    
    add: function(selector) {
        /// <summary>Adds one or more elements to the set of matched elements.</summary>
        /// <param name="selector">
        ///   1. #ID - The ID of a specific element.
        ///   2. .class - Specifies to retrieve elements with the specified class name.
        ///   3. DOM elements
        /// </param>
        
        var newElements = $FX(selector, this._document);
        
        for (var i = 0; i < newElements.length; ++i) {
            this[this.length + i] = newElements[i];
        }
        
        this.length += newElements.length;
        
        return this;
    },
    
    append: function() {
        /// <summary>
        ///     Append content to the inside of every matched element.
        ///     This operation is similar to doing an appendChild to all the
        ///     specified elements, adding them into the document.
        /// </summary>
        /// <returns type="FXDom" />
        
        return this.domManip(arguments, true, false, function(elem){
            if (this.nodeType == 1) {
                this.appendChild(elem);
            }
        });
    },
    
    prepend: function() {
        /// <summary>
        ///     Prepends content to the inside of every matched element.
        ///     This operation is similar to doing an appendChild to all the
        ///     specified elements, adding them into the document.
        /// </summary>
        /// <returns type="FXDom" />
        
        return this.domManip(arguments, true, true, function(elem){
            if (this.nodeType == 1) {
                this.insertBefore(elem, this.firstChild);
            }
        });
    },
    
    before: function() {
        /// <summary>
        ///     Insert content before each of the matched elements.
        /// </summary>
        /// <returns type="FXDom" />

        return this.domManip(arguments, false, false, function(elem){
            this.parentNode.insertBefore(elem, this);
        });
    },
    
    after: function() {
        /// <summary>
        ///     Insert content after each of the matched elements.
        /// </summary>
        /// <returns type="FXDom" />

        return this.domManip(arguments, false, true, function(elem){
            this.parentNode.insertBefore(elem, this.nextSibling);
        });
    },

    replaceWith: function() {
        /// <summary>
        /// Replaces the selected elements with the specified arguments.
        /// </summary>
        /// <returns type="FXDom" />

        var elems = this.clean(arguments, this.ownerDocument );
        this.before(elems).remove();

        return $FX(elems, this._document);
    },
    
    domManip: function(args, table, reverse, callback) {
        /// <summary>Manipulates the DOM using the specified elements and a callback to do the actual work.</summary>
        /// <param name="args" type="Array">The HTML elements used to manipulate the DOM.</param>
        /// <param name="table" type="Boolean">True to insert a TBODY in TABLEs if one is not found.</param>
        /// <param name="reverse type="Boolean">True to reverse the elements in args.</param>
        /// <param name="callback" type="Function">The function doing the DOM manipulation.</param>
        /// <returns type="FXDom" />
        
        var clone = this.length > 1, elems;
        
        var that = this;
        return this.each(function() {
            if (!elems) {
                elems = that.clean( args, this.ownerDocument );
                if (reverse) {
                    elems.reverse();
                }
            }
            
            var obj = this;
            
            if (table && FXDom.nodeName(this, "table") && FXDom.nodeName(elems[0], "tr")) {
                var obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") );
            }

            FXDom.each(elems, function(){
                var elem = clone ?
                    that.clone(this) :
                    this;

                elem = elem.get && elem.get(0) || elem;

                // execute all scripts after the elements have been injected
                if (!FXDom.nodeName( elem, "script")) {
                    // Inject the elements into the document
                    callback.call( obj, elem );
                }
            });
        });
    },
    
    attr: function(name, value) {
        /// <summary>
        ///    Gets an attribute of the first element in this FXDom object
        ///    or sets multiple attributes for all elements in this FXDom object.
        /// </summary>
        /// <param name="name" type="Object">
        ///    The name of a single attribute or an object that specifies the names
        ///    and values of attributes to set.
        /// </param>
        /// <param name="value" type="Object">
        ///    The value of the single attribute to set.
        /// </param>
        /// <returns type="FXDom" />
    
        var options = name;
        
        if (name.constructor === String) {
            if (value === undefined) {
                return FXDom.attr(this[0], name);
            }
            else {
                options = {};
                options[name] = value;
            }
        }
        
        return this.each(function(i) {
            for (name in options) {
                FXDom.attr(this, name, options[name]);
            }
        });
    },
    
    css: function(name, value) {
        /// <summary>
        ///    Gets a style attribute of the first element in this FXDom object
        ///    or sets multiple attributes for all elements in this FXDom object.
        /// </summary>
        /// <param name="name" type="Object">
        ///    The name of a single style attribute or an object that specifies the names
        ///    and values of style attributes to set.
        /// </param>
        /// <param name="value" type="Object">
        ///    The value of the single style attribute to set.
        /// </param>
        /// <returns type="FXDom" />
    
        var options = name;
        
        if (name.constructor === String) {
            if (value === undefined) {
                return FXDom.css(this[0], name);
            }
            else {
                options = {};
                options[name] = value;
            }
        }
        
        return this.each(function(i) {
            for (name in options) {
                FXDom.css(this, name, options[name]);
            }
        });
    },
    
    empty: function() {
        /// <summary>Removes all content from the selected elements.</summary>
        /// <returns type="FXDom" />
        
        this.each(
            function(i, element) {
                element.innerHTML = "";
            }
        );
        
        return this;
    },
    
    get: function(index) {
        /// <summary>
        ///    Gets all DOM elements in an array, if index is not specified. Otherwise, gets the DOM element
        ///    at the specified index, if it exists.
        /// </summary>
        /// <param name="index" optional="true">(Optional) The index of a DOM element to retrieve.
        /// <returns type="Object">A DOM element or an array of DOM elements.</returns>

        return index === undefined ? FXDom.makeArray(this) : this[index];
    },
    
    html: function(value) {
        /// <summary>Gets or sets the value attribute of each element.</summary>
        /// <param name="value">The new value.</param>
        
        return this.attr("innerHTML", value);
    },

    outerHtml: function() {
        /// <summary>Returns the HTML, including this node, of the first selected element.</summary>
        /// <returns type="String" />

        var clone = this.clone(this.get(0));
        return $FX("<p>", this._document).append(clone).html();
    },
    
    parent: function() {
        /// <summary>Selects the parents of all currently selected elements.</summary>
        /// <returns type="FXDom" />
        
        var parents = [];
        
        this.each(
            function(i, element) {
                parents.push(element.parentNode);
            }
        );
        
        return $FX(parents);
    },
    
    remove: function() {
        /// <summary>Removes all currently selected elements from the DOM tree.</summary>
        
        this.each(
            function(i, element) {
                element.parentNode.removeChild(element);
            }
        );
        
        return this;
    },
    
    val: function(value) {
        /// <summary>Gets or sets the value attribute of each element.</summary>
        /// <param name="value">The new value.</param>
        
        return this.attr("value", value);
    }
};

FXDom.prototype.init.prototype = FXDom.prototype;

FXDom.attr = function(element, name, value) {
    /// <summary>Gets or sets an attribute on a particular element.</summary>
    /// <param name="element" domElement="true">The DOM element with which to get or set an attribute.</param>
    /// <param name="name">The name of the attribute to get or set.</param>
    /// <param name="value">The value of the attribute to set.</param>
    /// <returns type="Object">The value of the attribute specified in the name parameter.</returns>
    
    if (element && value === undefined) {
        value = element[name];

        if (value === undefined) {
            return element.getAttribute(name);
        }

        return value;
    }
    
    if (element[name] !== undefined) {
        element[name] = value;
    }
    else {
        element.setAttribute(name, value);
    }

    return value;
};

FXDom.css = function(element, name, value) {
    /// <summary>Gets or sets a style attribute on a particular element.</summary>
    /// <param name="element" domElement="true">The DOM element with which to get or set an attribute.</param>
    /// <param name="name">The name of the attribute to get or set.</param>
    /// <param name="value">The value of the attribute to set.</param>
    /// <returns type="Object">The value of the attribute specified in the name parameter.</returns>
    
    if (element && value === undefined) {
        return element.style[name];
    }
    
    element.style[name] = value;
    
    return value;
};

FXDom.nodeName = function(element, name) {
    /// <summary>
    ///     Checks whether the specified element has the specified DOM node name.
    /// </summary>
    /// <param name="element" type="Element">The element to examine</param>
    /// <param name="name" type="String">The node name to check</param>
    /// <returns type="Boolean">True if the specified node name matches the node's DOM node name; otherwise false</returns>

    return element.nodeName && element.nodeName.toUpperCase() === name.toUpperCase();
};

FXDom.makeArray = function(array) {
    /// <summary>
    ///     Turns anything into a true array.  This is an internal method.
    /// </summary>
    /// <param name="array" type="Object">Anything to turn into an actual Array</param>
    /// <returns type="Array" />
    /// <private />

    var ret = [];

    if(array) {
        var length = array.length;
        //the window, strings and functions also have 'length'
        if (length === undefined || array.split || array.setInterval || array.call) {
            ret[0] = array;
        }
        else {
            while(length) {
                ret[--length] = array[length];
            }
        }
    }
    
    return ret;
};

FXDom.each = function(object, callback, context) {
    /// <summary>Iterates over an array or an object's properties until the callback function returns false.</summary>
    /// <param name="object" type="Object">The object whose indices or properties should be iterated.</param>
    /// <param name="callback" type="Function">The callback function to invoke for each argument of the array.</param>
    /// <param name="context" type="Object" optional="true">(Optional) The context (i.e. the this object) in which the callback is called.</param>

    var length = object.length;

    if (typeof(length) === "number") {
        for (var i = 0; i < length; ++i) {
            var value = object[i];
            if (callback.call(context || value, i, value) === false) {
                break;
            }
        }
    }
    else {
        for (var i in object) {
            if (callback.call(context || object[i], i, object[i]) === false) {
                break;
            }
        }
    }
    
    return object;
};

FXDom.clone = function(element) {
    var clone = element.cloneNode(true);
        
    // Clone the event handlers
    if (element.eventHandlers) {
        var handlers = element.eventHandlers;
        if (handlers) {
            var newHandlers = clone.eventHandlers = {};
                
            for (var e in handlers) {
                newHandlers[e] = handlers[e];
                    
                clone.addEventListener(e, newHandlers[e], false);
            }
        }
    }
        
    return clone;
};

FXDom.each(
    "blur,command,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,change,select,submit,keydown,keypress,keyup,error".split(","),
    function(i, name) {
        FXDom.prototype[name] = function(callback, removeExisting) {
            if (callback) {
                var that = this;
                this.each(function() {
                    var handlers = this.eventHandlers;
                
                    if (!handlers) {
                        handlers = this.eventHandlers = {};
                    }

                    if (removeExisting) {
                        var existingHandler = handlers[name];
                        if (existingHandler) {
                            this.removeEventListener(name, existingHandler, false);
                        }
                    }
                
                    handlers[name] = callback;
                    this.addEventListener(name, callback, false);
                });
            }
            else {
                this.each(function() {
                    if (this.eventHandlers) {
                        this.eventHandlers[name].apply(this);
                    }
                });
            }
            
            return this;

        };
    }
);

FXDom.filters = {};

FXDom.filters.first = {
    filterList: function(matches, data) {
        return [ matches[0] ];
    }
};

FXDom.filters.last = {
    filterList: function(matches, data) {
        return [ matches[matches.length - 1] ];
    }
};

FXDom.filters.eq = {
    filterList: function(matches, data) {
        return [ matches[data - 1] ];
    }
};

FXDom.filters.even = {
    filterInline: function(element, data, index) {
        return ((index + 1) & 1) === 0;
    }
};

FXDom.filters.contains = {
    filterInline: function(element, data, index) {
        return $.contains(element.innerHTML, data);
    }
};

}});